﻿using ImageMagick;
using Microsoft.AspNetCore.Http;
using PMS.Core.Configuration.CommonEnumConfigs;
using PMS.Core.Infrastructure;
using PMS.Core.Utils;
using PMS.Data.DbUtils;
using PMS.Data.Entities.Medias;
using PMS.Data.Entities.Settings;
using PMS.Services.Caching.Extensions;
using PMS.Services.Utils;
using System;
using System.Drawing;
using System.IO;
using System.Linq;

namespace PMS.Services.EntityServices.Medias
{
    /// <summary>
    /// Resize types
    /// </summary>
    public enum ResizeType
    {
        /// <summary>
        /// Longest side
        /// </summary>
        LongestSide,

        /// <summary>
        /// Width
        /// </summary>
        Width,

        /// <summary>
        /// Height
        /// </summary>
        Height
    }
    public abstract class BaseMediaFileService : IMediaFileService
    {
        #region Fields
        private const int DEFAULTIMAGEMAXWIDTH = 1920;
        private readonly MediaSettings _mediaSettings;
        private readonly IPMSFileProvider _fileProvider;
        private readonly IEventDispatcher _eventDispatcher;
        private readonly IHttpContextAccessor _httpContextAccessor;
        private readonly IRepository<MediaFile> _mediaFileRepository;
        #endregion

        #region Ctor
        public BaseMediaFileService(MediaSettings mediaSettings,
            IEventDispatcher eventDispatcher,
            IHttpContextAccessor httpContextAccessor,
            IRepository<MediaFile> mediaFileRepository)
        {
            _mediaSettings = mediaSettings;
            _fileProvider = CommonHelper.DefaultFileProvider;
            _eventDispatcher = eventDispatcher;
            _httpContextAccessor = httpContextAccessor;
            _mediaFileRepository = mediaFileRepository;
        }
        #endregion

        #region Utils
        /// <summary>
        /// Get MediaFile path URL 
        /// </summary>
        /// <returns></returns>
        protected virtual string GetMediaFilePathUrl(string virtualPath = "images")
        {
            var pathBase = _httpContextAccessor.HttpContext.Request.PathBase.Value ?? string.Empty;
            if (virtualPath.StartsWith("/")) virtualPath = virtualPath.Substring(1);
            if (virtualPath.EndsWith("/")) virtualPath = virtualPath.Substring(0, virtualPath.Length - 1);
            var filePathUrl = $"{pathBase}/{virtualPath}/";

            return filePathUrl;
        }
        /// <summary>
        /// Get picture (thumb) local path
        /// </summary>
        /// <param name="thumbFileName">Filename</param>
        /// <returns>Local picture thumb path</returns>
        protected virtual string GetThumbLocalPath(string thumbFileName)
        {
            var thumbsDirectoryPath = _fileProvider.GetAbsolutePath(MediaDefaultConfigs.ImageThumbsPath);
            if (!_fileProvider.DirectoryExists(thumbsDirectoryPath))
                _fileProvider.CreateDirectory(thumbsDirectoryPath);

            //get the first two letters of the file name
            var fileNameWithoutExtension = _fileProvider.GetFileNameWithoutExtension(thumbFileName);
            if (fileNameWithoutExtension != null && fileNameWithoutExtension.Length > MediaDefaultConfigs.MultipleThumbDirectoriesLength)
            {
                var subDirectoryName = fileNameWithoutExtension.Substring(0, MediaDefaultConfigs.MultipleThumbDirectoriesLength);
                thumbsDirectoryPath = _fileProvider.GetAbsolutePath(MediaDefaultConfigs.ImageThumbsPath, subDirectoryName);
                _fileProvider.CreateDirectory(thumbsDirectoryPath);
            }

            var thumbFilePath = _fileProvider.Combine(thumbsDirectoryPath, thumbFileName);
            return thumbFilePath;
        }

        /// <summary>
        /// Save a value indicating whether some file (thumb) already exists
        /// </summary>
        /// <param name="thumbFilePath">Thumb file path</param>
        /// <param name="binary">Picture binary</param>
        protected virtual void SaveThumb(string thumbFilePath, byte[] binary)
        {
            //ensure \thumb directory exists
            var thumbsDirectoryPath = _fileProvider.GetAbsolutePath(MediaDefaultConfigs.ImageThumbsPath);
            if (!_fileProvider.DirectoryExists(thumbsDirectoryPath))
                _fileProvider.CreateDirectory(thumbsDirectoryPath);

            //save
            _fileProvider.WriteAllBytes(thumbFilePath, binary);
        }

        /// <summary>
        /// Delete picture thumbs Only when MediaFile is Picture
        /// </summary>
        /// <param name="picture">Picture</param>
        protected virtual void DeletePictureThumbs(MediaFile imageFile)
        {
            var fileCategory = MimeTypesManager.Instance.GetFileCategoryFromExtName(imageFile.ExtensionName);
            if (fileCategory != MediaFileType.Image) return;
            var filter = $"{imageFile.Id:0000000}*.*";
            var currentFiles = _fileProvider.GetFiles(_fileProvider.GetAbsolutePath(MediaDefaultConfigs.ImageThumbsPath), filter, false);
            foreach (var currentFileName in currentFiles)
            {
                var thumbFilePath = GetThumbLocalPath(currentFileName);
                _fileProvider.DeleteFile(thumbFilePath);
            }
        }
        protected virtual string GenerateUniqueMediaFileName()
        {
            return Guid.NewGuid().ToString("N");  //32位字符串（没有短横线）
        }

        /// <summary>
        /// Calculates picture dimensions whilst maintaining aspect
        /// </summary>
        /// <param name="originalSize">The original picture size</param>
        /// <param name="targetSize">The target picture size (longest side)</param>
        /// <param name="resizeType">Resize type</param>
        /// <param name="ensureSizePositive">A value indicating whether we should ensure that size values are positive</param>
        /// <returns></returns>
        protected virtual Size CalculateDimensions(Size originalSize, int targetSize,
            ResizeType resizeType = ResizeType.LongestSide, bool ensureSizePositive = true)
        {
            float width, height;

            switch (resizeType)
            {
                case ResizeType.LongestSide:
                    if (originalSize.Height > originalSize.Width)
                    {
                        // portrait
                        width = originalSize.Width * (targetSize / (float)originalSize.Height);
                        height = targetSize;
                    }
                    else
                    {
                        // landscape or square
                        width = targetSize;
                        height = originalSize.Height * (targetSize / (float)originalSize.Width);
                    }

                    break;
                case ResizeType.Width:
                    width = targetSize;
                    height = originalSize.Height * (targetSize / (float)originalSize.Width);
                    break;
                case ResizeType.Height:
                    width = originalSize.Width * (targetSize / (float)originalSize.Height);
                    height = targetSize;
                    break;
                default:
                    throw new Exception("Not supported ResizeType");
            }

            if (!ensureSizePositive)
                return new Size((int)Math.Round(width), (int)Math.Round(height));

            if (width < 1)
                width = 1;
            if (height < 1)
                height = 1;

            //we invoke Math.Round to ensure that no white background is rendered - https://www.nopcommerce.com/boards/t/40616/image-resizing-bug.aspx
            return new Size((int)Math.Round(width), (int)Math.Round(height));
        }
        /// <summary>
        /// Resize input picture dimensions
        /// </summary>
        /// <param name="pictureBinary">Picture binary</param>
        /// <param name="keepAspectRatio">
        /// 是否保持输入源图的宽高比进行尺寸缩放
        /// true: 保持源图大小宽高比对图片尺寸进行调整
        /// false:按照参数resizeWidth及resizeHeight对图片进行尺寸调整
        /// </param>
        /// <param name="resizeWidth">
        /// 图片Resize后的宽度，0则根据系统设定的图片最大尺寸，超过才Resize缩放
        /// </param>
        /// <param name="resizeHeight">同resizeWidth</param>
        /// <returns>Picture binary or throws an exception</returns>
        protected virtual byte[] ResizePicture(byte[] pictureBinary, bool keepAspectRatio = true, int resizeWidth = 0, int resizeHeight = 0)
        {
            using var image = new MagickImage(pictureBinary);
            var newSize = new MagickGeometry();
            if (keepAspectRatio)
            {
                newSize.IgnoreAspectRatio = false;
                int defaultMaxImgWidth = _mediaSettings.MaximumImageWidth <= 0
                                       ? DEFAULTIMAGEMAXWIDTH
                                       : _mediaSettings.MaximumImageWidth;
                if (resizeWidth <= 0 && defaultMaxImgWidth > 0)
                {
                    newSize.Width = Math.Min(image.Width, defaultMaxImgWidth);
                    image.Resize(newSize);
                    return image.ToByteArray();
                }
                else if (resizeWidth > 0)
                {//Ignore resizeHeight when keepAspectRatio
                    newSize.Width = resizeWidth;
                    image.Resize(newSize);
                    return image.ToByteArray();
                }
                else
                {
                    //do nothing when resizeWidth==0&&defaultImgWidth==0
                    return image.ToByteArray();
                }
            }
            else
            {
                // This will resize the image to a fixed size without maintaining the aspect ratio.
                // Normally an image will be resized to fit inside the specified size.
                newSize.IgnoreAspectRatio = true;
                if (resizeWidth <= 0 || resizeHeight <= 0)
                {
                    int defaultMaxImgWidth = _mediaSettings.MaximumImageWidth <= 0
                                           ? DEFAULTIMAGEMAXWIDTH
                                           : _mediaSettings.MaximumImageWidth;
                    Size originSize = new Size(image.Width, image.Height);
                    Size targetSize = CalculateDimensions(originSize, defaultMaxImgWidth);
                    newSize.Width = targetSize.Width;
                    newSize.Height = targetSize.Height;
                } 
                else
                {
                    newSize.Width = resizeWidth;
                    newSize.Height = resizeHeight;
                }
               
                image.Resize(newSize);
                return image.ToByteArray();
            }
        }

        /// <summary>
        /// Try Compress Image when image size larger than ImageMaximumSizeBytes
        /// </summary>
        /// <param name="pictureBinary"></param>
        /// <returns></returns>
        protected virtual byte[] CompressPicture(byte[] pictureBinary)
        {
            if (pictureBinary == null) return null;
            int imgSize = pictureBinary.Length;
            if (imgSize > _mediaSettings.ImageCompressBytes)
            {
                using var stream = new MemoryStream(pictureBinary);
                var optimizer = new ImageOptimizer();
                optimizer.Compress(stream);
                return stream.ToArray();
            }
            return pictureBinary;
        }

        /// <summary>
        /// Gets the MediaFile binary array
        /// </summary>
        /// <param name="file">File</param>
        /// <returns>MediaFile binary array</returns>
        protected virtual byte[] GetMediaFileBits(IFormFile file)
        {
            using var fileStream = file.OpenReadStream();
            using var ms = new MemoryStream();
            fileStream.CopyTo(ms);
            var fileBytes = ms.ToArray();
            return fileBytes;
        }

        /// <summary>
        /// Returns the file extension from mime type.
        /// </summary>
        /// <param name="mimeType">Mime type</param>
        /// <returns>File extension</returns>
        public virtual string GetOutputFileExtensionFromMimeType(string mimeType)
        {
            if (string.IsNullOrEmpty(mimeType))
                throw new ArgumentNullException(nameof(mimeType));
            var parts = mimeType.Split('/');
            var lastPart = parts[parts.Length - 1];
            bool keepSearch = true;
            switch (lastPart) //For Common Image Extensions
            {
                case "pjpeg":
                    lastPart = "jpg";
                    keepSearch = false;
                    break;
                case "x-png":
                    lastPart = "png";
                    keepSearch = false;
                    break;
                case "x-icon":
                    lastPart = "ico";
                    keepSearch = false;
                    break;
                case "jpeg":
                    lastPart = "jpg";
                    keepSearch = false;
                    break;
                case "jpg":
                case "png":
                case "gif":
                case "bmp":
                case "webp":
                    return lastPart;
                default:
                    break;
            }

            if (!keepSearch) return lastPart;
            return MimeTypesManager.Instance.GetFileOutputExtNameByMimeType(mimeType);
        }
        /// <summary>
        /// Returns the file extension from filename like: nopic.png
        /// Note: All Files Uploaded from client-side, should restore with filename and file extension
        /// eg: .jpeg or .jpg file all restore with extension name: .jpg 
        /// </summary>
        /// <param name="fileName">fileName and its extension name like: nopic.png</param>
        /// <returns></returns>
        public virtual string GetOutputFileExtensionFromFileName(string fileName)
        {
            string ext = _fileProvider.GetFileExtension(fileName).Replace(".", ""); // extension name like jpg
            return MimeTypesManager.Instance.GetFileOutputExtName(ext);
        }
        /// <summary>
        /// Gets the default Picture URL
        /// </summary>
        /// <param name="targetSize">The target picture size (longest side)</param>
        /// <returns>Picture URL</returns>
        public virtual string GetDefaultPictureUrl(int targetSize = 0)
        {
            var filePath = _fileProvider.GetAbsolutePath(MediaDefaultConfigs.DefaultImagePathLocal);
            if (!_fileProvider.FileExists(filePath))
            {
                return string.Empty;
            }

            if (targetSize == 0)
            {
                var url = GetMediaFilePathUrl("images") + "default-image.png";

                return url;
            }
            else
            {
                var fileExtension = _fileProvider.GetFileExtension(filePath);
                var thumbFileName = $"{_fileProvider.GetFileNameWithoutExtension(filePath)}_{targetSize}{fileExtension}";
                var thumbFilePath = GetThumbLocalPath(thumbFileName);

                try
                {
                    if (!_fileProvider.FileExists(thumbFilePath))
                    {
                        _fileProvider.FileCopy(filePath, thumbFilePath, true);
                        var pictureBinary = ResizePicture(_fileProvider.ReadAllBytes(thumbFilePath), true, targetSize);
                        SaveThumb(thumbFilePath, pictureBinary);
                    }
                } 
                catch(Exception)
                {
                    return GetMediaFilePathUrl("images") + "default-image.png";
                }

                var url = GetMediaFilePathUrl(_fileProvider.GetVirtualPath(thumbFilePath).TrimStart('~')) + thumbFileName;
                return url;
            }
        }
        /// <summary>
        /// Get a Media File URL depends on media storage settings
        /// the default picture is shown when file not found
        /// </summary>
        /// <param name="fileId">Media file identifier</param>
        /// <param name="targetSize">The target picture size (longest side), only available when target is image type</param>
        /// <returns>
        /// Returns a ImageNotFound.jpg url as default url when media file not exists(deleted or unexpected situations)
        /// IsDefault : true, means target MediaFile not found(deleted or whatever), FileUrl is the default ImageFile Url(ImageNotFound.jpg) instead
        ///           : otherwise returns the target media file url
        /// </returns>
        public virtual (bool IsDefault, string FileUrl, string thumbFileUrl) GetMediaFileUrl(int fileId, int targetSize = 0)
        {
            var file = GetMediaFileById(fileId);
            return GetMediaFileUrl(ref file, targetSize);
        }
        public abstract (bool IsDefault, string FileUrl, string thumbFileUrl) GetMediaFileUrl(ref MediaFile media, int targetSize = 0);
        /// <summary>
        /// Gets a MediaEntity
        /// </summary>
        /// <param name="fileId">Media file identifier</param>
        /// <returns>MediaFile</returns>
        public virtual MediaFile GetMediaFileById(int fileId)
        {
            if (fileId == 0)
                return null;

            return _mediaFileRepository.ToCachedGetById(fileId);
        }

        public abstract bool DeleteMediaFile(MediaFile file);
        /// <summary>
        /// Gets a collection of media files
        /// </summary>
        /// <param name="pageIndex">Current page</param>
        /// <param name="pageSize">Items on each page</param>
        /// <returns>Paged list of media files</returns>
        public virtual IPagedList<MediaFile> GetMediaFiles(int pageIndex = 0, int pageSize = int.MaxValue)
        {
            var query = _mediaFileRepository.Table.OrderByDescending(p => p.UpdatedOnTimeUtc);

            return new PagedList<MediaFile>(query, pageIndex, pageSize);
        }
        public abstract byte[] LoadPictureBinary(MediaFile imageFile);
        public abstract MediaFile InsertPicture(byte[] pictureBinary, string mimeType, string useage = null, string title = null, string description = null, string imagelink = null);
        public abstract MediaFile UpdatePicture(int pictureId, string mimeType, byte[] pictureBinary = null, string useage = null, string title = null, string description = null, string imagelink = null);
        public abstract MediaFile InsertMediaFile(IFormFile formFile, string useage = null, string title = null, string description = null, string imagelink = null);
        public abstract MediaFile UpdateMediaFile(int fileId, IFormFile formFile, string useage = null, string title = null, string description = null, string imagelink = null);
        /// <summary>
        /// Updates the media file info only
        /// param for update: Id, Title, Description, Useage, ImageLink
        /// </summary>
        /// <param name="file">The MediaFile to update</param>
        /// <returns>Picture</returns>
        public virtual MediaFile UpdateMediaFileInfoOnly(MediaFile file)
        {
            if (file == null) return null;
            else if (file.Id <= 0) return null;

            var targetFile = GetMediaFileById(file.Id);
            if (targetFile == null) return null;
            targetFile.SeoFilename = string.IsNullOrEmpty(file.SeoFilename) ? GenerateUniqueMediaFileName() : file.SeoFilename;
            targetFile.ExtensionName = MimeTypesManager.Instance.GetFileOutputExtName(file.ExtensionName);
            targetFile.VirtualPath = string.IsNullOrEmpty(file.VirtualPath)
                                   ? throw new PMSException("VirtualPath cannot be null")
                                   : file.VirtualPath;
            targetFile.Useage = string.IsNullOrEmpty(file.Useage) ? targetFile.Useage : file.Useage;
            targetFile.Title = string.IsNullOrEmpty(file.Title) ? targetFile.Title : file.Title;
            targetFile.Description = string.IsNullOrEmpty(file.Description) ? targetFile.Description : file.Description;
            targetFile.ImageLink = string.IsNullOrEmpty(file.ImageLink) ? targetFile.ImageLink : file.ImageLink;
            targetFile.StoreMode = file.StoreMode;//MediaFileStoreMode.LocalFile;
            targetFile.UpdatedOnTimeUtc = DateTime.UtcNow;
            if (string.IsNullOrEmpty(targetFile.ExtensionName))
                throw new PMSException("MediaFile Extension Name cannot be Empty", SystemLogLevel.Error);
            else
                targetFile.ExtensionName = targetFile.ExtensionName.ToLowerInvariant();
            _mediaFileRepository.Update(targetFile);
            //event notification
            _eventDispatcher.EntityUpdated(targetFile);
            return targetFile;
        }
        #endregion
    }
}
